spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { notFound } from 'next/navigation';3import { Section, KV } from '@/components/ui/section';4import { Badge, StatusBadge } from '@/components/ui/badge';5import { JsonView } from '@/components/ui/json-view';6import { getRun } from '@/lib/queries/sources';7import { fmtDateTime, fmtDuration, fmtInt } from '@/lib/format';89export default async function RunPage({ params }: { params: Promise<{ runId: string }> }) {10 const { runId } = await params;11 const r = await getRun(runId);12 if (!r) notFound();13 return (14 <div className="space-y-8">15 <header className="pt-4">16 <p className="ci-kicker">Ingest run</p>17 <h1 className="ci-mono text-2xl font-sans">{r.id}</h1>18 <p className="mt-1 flex flex-wrap items-center gap-2 text-[13px]">19 <Link className="ci-link" href={`/source/${r.source_slug}`}>20 {r.source_name ?? r.connector_id}21 </Link>22 <StatusBadge status={r.status} />23 <Badge tone="outline">{r.mode}</Badge>24 {r.anomaly ? <Badge tone="danger">anomaly: {r.anomaly}</Badge> : null}25 </p>26 </header>27 <div className="grid gap-8 lg:grid-cols-[1fr_1fr]">28 <Section id="counts" kicker="Counts" title="Records and requests">29 <KV30 items={[31 { k: 'Started', v: fmtDateTime(r.started_at) },32 { k: 'Finished', v: r.finished_at ? fmtDateTime(r.finished_at) : 'running' },33 { k: 'Duration', v: fmtDuration(r.duration_ms) },34 { k: 'Fetched', v: <span className="ci-num">{fmtInt(r.records_fetched)}</span> },35 { k: 'Created', v: <span className="ci-num">{fmtInt(r.records_created)}</span> },36 { k: 'Updated', v: <span className="ci-num">{fmtInt(r.records_updated)}</span> },37 { k: 'Unchanged', v: <span className="ci-num">{fmtInt(r.records_unchanged)}</span> },38 { k: 'Rejected', v: <span className="ci-num">{fmtInt(r.records_rejected)}</span> },39 { k: 'Validation failures', v: <span className="ci-num">{fmtInt(r.validation_failures)}</span> },40 { k: 'HTTP requests / failures', v: <span className="ci-num">{fmtInt(r.http_requests)} / {fmtInt(r.http_failures)}</span> },41 { k: 'Rate-limit events', v: <span className="ci-num">{fmtInt(r.rate_limit_events)}</span> },42 { k: 'Dataset version', v: r.dataset_version ? <span className="ci-mono">{r.dataset_version}</span> : null },43 { k: 'Error', v: r.error ? <span className="text-danger">{r.error}</span> : null },44 ]}45 />46 </Section>47 <Section id="cursor" kicker="Restartability" title="Cursor before / after">48 <div className="grid gap-4 sm:grid-cols-2">49 <div>50 <p className="ci-kicker mb-1">Before</p>51 <JsonView data={r.cursor_before} />52 </div>53 <div>54 <p className="ci-kicker mb-1">After</p>55 <JsonView data={r.cursor_after} />56 </div>57 </div>58 </Section>59 </div>60 <Section id="drift" kicker="Schema drift" title={`Drift signals (${r.schema_drift?.length ?? 0})`}>61 {r.schema_drift?.length ? <JsonView data={r.schema_drift} /> : <p className="text-[13px] text-ink-3">No schema drift detected in this run.</p>}62 </Section>63 <Section id="log" kicker="Log" title={`Log (${r.log?.length ?? 0} lines)`}>64 {r.log?.length ? (65 <pre className="ci-code max-h-[600px] overflow-auto">66 {r.log.map((l, i) => (67 <div key={i} className={l.level === 'error' ? 'text-danger' : l.level === 'warn' ? 'text-warn' : ''}>68 <span className="text-ink-3">{l.t}</span> <span className="uppercase">{l.level.padEnd(5)}</span> {l.msg}69 </div>70 ))}71 </pre>72 ) : (73 <p className="text-[13px] text-ink-3">Empty log.</p>74 )}75 </Section>76 </div>77 );78}79